from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.functional import classproperty

from sysreptor.pentests import cvss as cvss_utils
from sysreptor.pentests import querysets
from sysreptor.pentests.fielddefinition.predefined_fields import FINDING_FIELDS_CORE
from sysreptor.pentests.models.common import (
    ImportableMixin,
    LanguageMixin,
    LockableMixin,
    ReviewStatus,
    get_risk_score_from_data,
)
from sysreptor.utils.decorators import cache
from sysreptor.utils.fielddefinition.mixins import CustomFieldsMixin
from sysreptor.utils.fielddefinition.types import FieldDefinition
from sysreptor.utils.fielddefinition.utils import HandleUndefinedFieldsOptions, ensure_defined_structure
from sysreptor.utils.fielddefinition.validators import FieldValuesValidator
from sysreptor.utils.history import HistoricalRecords
from sysreptor.utils.models import BaseModel
from sysreptor.utils.utils import omit_keys


class FindingTemplate(LockableMixin, ImportableMixin, BaseModel):
    main_translation = models.ForeignKey(to='FindingTemplateTranslation', null=True, on_delete=models.SET_NULL)

    tags = ArrayField(
            base_field=models.CharField(max_length=255),
            default=list, blank=True, db_index=True)

    usage_count = models.PositiveIntegerField(default=0, db_index=True)
    copy_of = models.ForeignKey(to='FindingTemplate', on_delete=models.SET_NULL, null=True, blank=True)

    history = HistoricalRecords(cascade_delete_history=True, excluded_fields=['usage_count'])
    objects = querysets.FindingTemplateManager()

    @classproperty
    @cache('FindingTemplate.field_definition', timeout=10)
    def field_definition(cls) -> FieldDefinition:
        return FindingTemplate.objects.get_field_definition()

    def is_file_referenced(self, f) -> bool:
        for translation in self.translations.all():
            # custom_fields is used instead of data_all,
            # because accessing data_all would require field_definition which triggers an additional unneeded DB query
            if f.name in str(translation.custom_fields):
                return True
        return False

    def copy(self, **kwargs):
        return FindingTemplate.objects.copy(self, **kwargs)


class FindingTemplateTranslation(CustomFieldsMixin, LanguageMixin, BaseModel):
    template = models.ForeignKey(to=FindingTemplate, on_delete=models.CASCADE, related_name='translations')

    status = models.CharField(max_length=255, default=ReviewStatus.IN_PROGRESS, db_index=True)
    title = models.TextField(default='', db_index=True)

    risk_score = models.FloatField(null=True, db_index=True)
    risk_level = models.CharField(max_length=10, choices=cvss_utils.CVSSLevel.choices, null=True, db_index=True)

    history = HistoricalRecords()
    objects = models.Manager.from_queryset(querysets.FindingTemplateTranslationQueryset)()

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['template', 'language'], deferrable=models.Deferrable.DEFERRED, name='unique_language_per_template'),
        ]

    @property
    def is_main(self) -> bool:
        if not self.id or not self.template_id or not self.template.main_translation_id:
            return None
        return self.template.main_translation_id == self.id

    @property
    def field_definition(self) -> dict:
        return FindingTemplate.field_definition

    @property
    def core_field_names(self) -> list[str]:
        return list(FINDING_FIELDS_CORE.keys())

    @property
    def data(self) -> dict:
        return self.get_data()

    @property
    def data_all(self) -> dict:
        return self.data

    def get_data(self, inherit_main=False) -> dict:
        # Build dict of all current values
        # Merge core fields stored directly on the model instance and custom_fields stored as dict
        data = self.custom_fields.copy() | {'title': self.title}

        # Use values of main_translation if not present
        if inherit_main and self.is_main is False:
            data = self.template.main_translation.data | data

        # recursively check for undefined fields and set default value
        return ensure_defined_structure(
            value=data,
            definition=FieldDefinition(fields=[f for f in self.field_definition.fields if f.id in data.keys()]),
            handle_undefined=HandleUndefinedFieldsOptions.FILL_NONE,
            include_unknown=True)

    def update_data(self, value):
        # Keep unknown fields
        value = omit_keys(self.data_all, self.field_definition.keys()) | value.copy()

        # Validate data
        FieldValuesValidator(self.field_definition, require_all_fields=False)(value)

        # Distribute to model fields
        self.title = value.pop('title', '') or ''
        self.custom_fields = value

    def __str__(self) -> str:
        return self.title

    def update_risk_score(self):
        # Update risk score and level
        r = get_risk_score_from_data(self.get_data(inherit_main=True))
        self.risk_score = r.get('score')
        self.risk_level = r.get('level')

    def save(self, *args, **kwargs):
        self.update_risk_score()
        return super().save(*args, **kwargs)

